/**
 *  StartAudioContext.js
 *  @author Yotam Mann
 *  @license http://opensource.org/licenses/MIT MIT License
 *  @copyright 2016 Yotam Mann
 */
(function (root, factory) {
    if (typeof define === "function" && define.amd) {
        define([], factory)
     } else if (typeof module === "object" && module.exports) {
        module.exports = factory()
    } else {
        root.StartAudioContext = factory()
  }
}(this, function () {

    //TAP LISTENER/////////////////////////////////////////////////////////////

    /**
     * @class  Listens for non-dragging tap ends on the given element
     * @param {Element} element
     * @internal
     */
    var TapListener = function(element, context){

        this._dragged = false

        this._element = element

        this._bindedMove = this._moved.bind(this)
        this._bindedEnd = this._ended.bind(this, context)

        element.addEventListener("touchmove", this._bindedMove)
        element.addEventListener("touchend", this._bindedEnd)
        element.addEventListener("mouseup", this._bindedEnd)
    }

    /**
     * drag move event
     */
    TapListener.prototype._moved = function(e){
        this._dragged = true
    };

    /**
     * tap ended listener
     */
    TapListener.prototype._ended = function(context){
        if (!this._dragged){
            startContext(context)
        }
        this._dragged = false
    };

    /**
     * remove all the bound events
     */
    TapListener.prototype.dispose = function(){
        this._element.removeEventListener("touchmove", this._bindedMove)
        this._element.removeEventListener("touchend", this._bindedEnd)
        this._element.removeEventListener("mouseup", this._bindedEnd)
        this._bindedMove = null
        this._bindedEnd = null
        this._element = null
    };

    //END TAP LISTENER/////////////////////////////////////////////////////////

    /**
     * Plays a silent sound and also invoke the "resume" method
     * @param {AudioContext} context
     * @private
     */
    function startContext(context){
        // this accomplishes the iOS specific requirement
        var buffer = context.createBuffer(1, 1, context.sampleRate)
        var source = context.createBufferSource()
        source.buffer = buffer
        source.connect(context.destination)
        source.start(0)

        // resume the audio context
        if (context.resume){
            context.resume()
        }
    }

    /**
     * Returns true if the audio context is started
     * @param  {AudioContext}  context
     * @return {Boolean}
     * @private
     */
    function isStarted(context){
         return context.state === "running"
    }

    /**
     * Invokes the callback as soon as the AudioContext
     * is started
     * @param  {AudioContext}   context
     * @param  {Function} callback
     */
    function onStarted(context, callback){

        function checkLoop(){
            if (isStarted(context)){
                callback()
            } else {
                requestAnimationFrame(checkLoop)
                if (context.resume){
                    context.resume()
                }
            }
        }

        if (isStarted(context)){
            callback()
        } else {
            checkLoop()
        }
    }

    /**
     * Add a tap listener to the audio context
     * @param  {Array|Element|String|jQuery} element
     * @param {Array} tapListeners
     */
    function bindTapListener(element, tapListeners, context){
        if (Array.isArray(element) || (NodeList && element instanceof NodeList)){
            for (var i = 0; i < element.length; i++){
                bindTapListener(element[i], tapListeners, context)
            }
        } else if (typeof element === "string"){
            bindTapListener(document.querySelectorAll(element), tapListeners, context)
        } else if (element.jquery && typeof element.toArray === "function"){
            bindTapListener(element.toArray(), tapListeners, context)
        } else if (Element && element instanceof Element){
            //if it's an element, create a TapListener
            var tap = new TapListener(element, context)
            tapListeners.push(tap)
        } 
    }

    /**
     * @param {AudioContext} context The AudioContext to start.
     * @param {Array|String|Element|jQuery} elements For iOS, the list of elements
     *                                               to bind tap event listeners
     *                                               which will start the AudioContext.
     * @param {Function=} callback The callback to invoke when the AudioContext is started.
     * @return {Promise} The promise is invoked when the AudioContext
     *                       is started.
     */
    function StartAudioContext(context, elements, callback){

        //the promise is invoked when the AudioContext is started
        var promise = new Promise(function(success) {
            onStarted(context, success)
        })

        // The TapListeners bound to the elements
        var tapListeners = []

        // add all the tap listeners
        if (elements){
            bindTapListener(elements, tapListeners, context)
        }

        //dispose all these tap listeners when the context is started
        promise.then(function(){
            for (var i = 0; i < tapListeners.length; i++){
                tapListeners[i].dispose()
            }
            tapListeners = null

            if (callback){
                callback()
            }
        })

        return promise
    }

    return StartAudioContext
}))